package com.atmosphere.component;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.atmosphere.component.base.ClocktimeComponent;
import com.atmosphere.interfaces.IAtmosphere;
import com.atmosphere.interfaces.IComponentKey;
import com.atmosphere.interfaces.component.IGeometryComponent;
import com.jme.animation.SpatialTransformer;
import com.jme.bounding.BoundingBox;
import com.jme.bounding.BoundingSphere;
import com.jme.image.Texture;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Geometry;
import com.jme.scene.Node;
import com.jme.scene.VBOInfo;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.shape.Dome;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.RenderState.StateType;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;

/**
 * <code>GeometryComponent</code> defines the implementation of
 * a component unit that represents the physical geometry that
 * forms the sky, cloud and other similar atmosphere components.
 *
 * @author Yi Wang (Neakor)
 * @version Creation date: 05-19-2009 16:23 EST
 * @version Modified date: 05-21-2009 19:46 EST
 */
public class GeometryComponent extends ClocktimeComponent implements IGeometryComponent {
	/**
	 * The update <code>Lock</code> for textures.
	 */
	private final Lock lock;
	/**
	 * The geometry scene graph parent <code>Node</code>.
	 */
	private Node parent;
	/**
	 * The <code>IAtmosphere</code> system.
	 */
	private IAtmosphere atmosphere;
	/**
	 * The use dome as default geometry <code>Boolean</code> flag.
	 */
	private volatile boolean dome;
	/**
	 * The day <code>Texture</code>.
	 */
	private Texture dayTex;
	/**
	 * The night <code>Texture</code>.
	 */
	private Texture nightTex;
	/**
	 * The transition <code>Texture</code>.
	 */
	private Texture transTexture;
	/**
	 * The <code>Geometry</code>.
	 */
	private Geometry geometry;
	/**
	 * The geometry rotation <code>SpatialTransformer</code>.
	 */
	private SpatialTransformer animator;
	/**
	 * The <code>Float</code> length of one rotation cycle.
	 */
	private volatile float rotationLength;
	/**
	 * The geometry dirty <code>Boolean</code> flag.
	 */
	private volatile boolean geomDirty;
	/**
	 * The rotation property dirty <code>Boolean</code> flag.
	 */
	private volatile boolean rotationDirty;
	/**
	 * The temporary initial <code>Texture</code>.
	 */
	private Texture initialTex;
	/**
	 * The temporary target <code>Texture</code>.
	 */
	private Texture targetTex;

	/**
	 * Constructor of <code>GeometryComponent</code>.
	 * @param key The <code>IComponentKey</code>.
	 */
	public GeometryComponent(IComponentKey key) {
		super(key);
		this.lock = new ReentrantLock();
	}

	@Override
	public void initialize(Node parent, IAtmosphere atmosphere) {
		this.parent = parent;
		this.atmosphere = atmosphere;
		this.initGeometry();
		this.initTextures();
		this.initRotation();
	}

	/**
	 * Initialize the geometry.
	 */
	private void initGeometry() {
		// Build the default geometry.
		if(this.geometry == null) {
			if(this.dome) {
				this.geometry = new Dome("Dome", new Vector3f(), 32, 32, 5, false);
				this.geometry.setModelBound(new BoundingSphere());
			} else {
				this.geometry = new Sphere("Sphere", new Vector3f(), 32, 32, 5);
				this.geometry.setModelBound(new BoundingBox());
			}
		}
		// Set up geometry.
		this.geometry.updateModelBound();
		this.geometry.setIsCollidable(false);
		this.geometry.setLightCombineMode(LightCombineMode.Off);
		this.geometry.setVBOInfo(new VBOInfo(true));
		// Create blend state.
		final BlendState blend = DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
		blend.setBlendEnabled(true);
		blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
		blend.setTestEnabled(true);
		blend.setTestFunction(BlendState.TestFunction.GreaterThan);
		blend.setEnabled(true);
		this.geometry.setRenderState(blend);
		this.geometry.updateRenderState();
		// Attach to parent.
		this.parent.detachChild(this.geometry);
		this.parent.attachChild(this.geometry);
		// Flip flag.
		this.geomDirty = false;
	}

	/**
	 * Initialize the blending textures for the geometry.
	 */
	private void initTextures() {
		this.initialTex = this.nightTex;
		this.targetTex = this.transTexture;
		this.prepareBlend();
		this.geometry.copyTextureCoordinates(0, 1, 1);
		this.geometry.updateRenderState();
	}

	/**
	 * Initialize the rotation properties.
	 */
	private void initRotation() {
		if(this.rotationLength <= 0) return;
		// Remove old controller.
		this.geometry.removeController(this.animator);
		// Setup new controller.
		this.animator = new SpatialTransformer(1);
		this.animator.setObject(this.geometry, 0, -1);
		final Quaternion start = new Quaternion();
		start.fromAngleAxis(0, new Vector3f(0,-1,0));
		this.animator.setRotation(0, 0, start);
		final Quaternion middle = new Quaternion();
		middle.fromAngleAxis(FastMath.DEG_TO_RAD*180, new Vector3f(0,-1,0));
		this.animator.setRotation(0, this.rotationLength/2.0f, middle);
		final Quaternion finish = new Quaternion();
		finish.fromAngleAxis(FastMath.DEG_TO_RAD*360, new Vector3f(0,-1,0));
		this.animator.setRotation(0, this.rotationLength, finish);
		this.animator.setActive(true);
		this.animator.setRepeatType(Controller.RT_WRAP);
		this.animator.interpolateMissing();
		this.geometry.addController(this.animator);
		// Flip flag.
		this.rotationDirty = false;
	}
	
	@Override
	protected void update(float clocktime, float interpolation) {
		this.updateDirty();
		this.lock.lock();
		try {
			this.updateBlending(clocktime);
		} finally {
			this.lock.unlock();
		}
	}

	/**
	 * Update dirty properties.
	 */
	private void updateDirty() {
		if(this.geomDirty) this.initialize(this.parent, this.atmosphere);
		else if(this.rotationDirty) this.initRotation();
	}
	
	/**
	 * Update the texture blending.
	 * @param clocktime The <code>Float</code> clock time.
	 */
	private void updateBlending(float clocktime) {
		// Day time.
		if(clocktime >= this.atmosphere.getDayStart() && clocktime < this.atmosphere.getDayEnd()) {
			if(this.initialTex != this.transTexture || this.targetTex != this.dayTex) {
				this.initialTex = this.transTexture;
				this.targetTex = this.dayTex;
				this.prepareBlend();
				this.initialTex.getBlendColor().a = 1;
			}
		// Else if the time is between day-end and day-transition, change towards transition texture.
		} else if(clocktime >= this.atmosphere.getDayEnd() && clocktime < this.atmosphere.getDayTransition()) {
			if(this.initialTex != this.dayTex || this.targetTex != this.transTexture) {
				this.initialTex = this.dayTex;
				this.targetTex = this.transTexture;
				this.prepareBlend();
			}
			this.initialTex.getBlendColor().a = ((clocktime-this.atmosphere.getDayEnd())/this.atmosphere.getDiffTransDay());
		// Else if the time is between day-transition and night-start, change towards night texture.
		} else if(clocktime >= this.atmosphere.getDayTransition() && clocktime < this.atmosphere.getNightStart()) {
			if(this.initialTex != this.transTexture || this.targetTex != this.nightTex) {
				this.initialTex = this.transTexture;
				this.targetTex = this.nightTex;
				this.prepareBlend();
			}
			this.initialTex.getBlendColor().a = ((clocktime-this.atmosphere.getDayTransition())/this.atmosphere.getDiffNightTrans());
		// Night time.
		} else if(clocktime >= this.atmosphere.getNightStart() || clocktime < this.atmosphere.getNightEnd()) {
			if(this.initialTex != this.transTexture || this.targetTex != this.nightTex) {
				this.initialTex = this.transTexture;
				this.targetTex = this.nightTex;
				this.prepareBlend();
				this.initialTex.getBlendColor().a = 1;
			}
		// Else if the time is between night-end and night-transition, change towards transition texture.
		} else if(clocktime >= this.atmosphere.getNightEnd() && clocktime < this.atmosphere.getNightTransition()) {
			if(this.initialTex != this.nightTex || this.targetTex != this.transTexture) {
				this.initialTex = this.nightTex;
				this.targetTex = this.transTexture;
				this.prepareBlend();
			}
			this.initialTex.getBlendColor().a = ((clocktime-this.atmosphere.getNightEnd())/this.atmosphere.getDiffTransNight());
		// If the time is between night-transition and day-start, change towards day texture.
		} else if(clocktime >= this.atmosphere.getNightTransition() && clocktime < this.atmosphere.getDayStart()) {
			if(this.initialTex != this.transTexture || this.targetTex != this.dayTex) {
				this.initialTex = this.transTexture;
				this.targetTex = this.dayTex;
				this.prepareBlend();
			}
			this.initialTex.getBlendColor().a = ((clocktime-this.atmosphere.getNightTransition())/this.atmosphere.getDiffDayTrans());
		}
	}
	
	/**
	 * Prepare texture blending.
	 */
	private void prepareBlend() {
		TextureState state = (TextureState)this.geometry.getRenderState(StateType.Texture);
		if(state == null) {
			state = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
			this.geometry.setRenderState(state);
		}
		// Prepare.
		state.setTexture(this.initialTex, 0);
		state.setTexture(this.targetTex, 1);
		this.initialTex.setApply(Texture.ApplyMode.Combine);
		this.initialTex.setCombineFuncRGB(Texture.CombinerFunctionRGB.Interpolate);
		this.initialTex.setCombineSrc0RGB(Texture.CombinerSource.TextureUnit1);
		this.initialTex.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
		this.initialTex.setCombineSrc1RGB(Texture.CombinerSource.CurrentTexture);
		this.initialTex.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
		this.initialTex.setCombineSrc2RGB(Texture.CombinerSource.Constant);
		this.initialTex.setCombineOp2RGB(Texture.CombinerOperandRGB.SourceAlpha);
		this.initialTex.setBlendColor(new ColorRGBA(1, 1, 1, 0));
		this.targetTex.setApply(Texture.ApplyMode.Combine);
		this.targetTex.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
		this.targetTex.setCombineSrc0RGB(Texture.CombinerSource.Previous);
		this.targetTex.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
		this.targetTex.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
		this.targetTex.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
	}
	
	@Override
	public void setDefaultDome(boolean dome) {
		this.dome = dome;
		this.geomDirty = true;
	}

	@Override
	public void setDayTexture(Texture texture) {
		this.lock.lock();
		try {
			this.dayTex = texture;
		} finally {
			this.lock.unlock();
		}
	}

	@Override
	public void setNightTexture(Texture texture) {
		this.lock.lock();
		try {
			this.nightTex = texture;
		} finally {
			this.lock.unlock();
		}
	}

	@Override
	public void setTransitionTexture(Texture texture) {
		this.lock.lock();
		try {
			this.transTexture = texture;
		} finally {
			this.lock.unlock();
		}
	}

	@Override
	public void setRotationLength(float length) {
		this.rotationLength = length;
		this.rotationDirty = true;
	}

	@Override
	public void setGeometry(Geometry geometry) {
		this.geometry = geometry;
		this.rotationDirty = true;
	}

	@Override
	public Texture getDayTexture() {
		return this.dayTex;
	}

	@Override
	public Texture getNightTexture() {
		return this.nightTex;
	}

	@Override
	public Texture getTransitionTexture() {
		return this.transTexture;
	}

	@Override
	public float getRotationLength() {
		return this.rotationLength;
	}

	@Override
	public Geometry getGeometry() {
		return this.geometry;
	}

	@Override
	public void cleanup() {
		this.parent.detachChild(this.geometry);
		TextureManager.releaseTexture(this.dayTex);
		TextureManager.releaseTexture(this.nightTex);
		this.dayTex = null;
		this.nightTex = null;
	}

	@Override
	public boolean isDefaultDome() {
		// TODO Auto-generated method stub
		return false;
	}
}
